﻿using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Linq;
using System.Windows.Controls.Primitives;
using System.Diagnostics;
using System.Collections.Generic;
using System.Collections.Specialized;
using Odyssey.Utils;
using System.Windows.Threading;

namespace Odyssey.Controls
{
    /// <summary>
    /// Fast replacement for VirtualizingStackPanel in case of Items with a fixed height.
    /// Unless VirtualizingStackPanel, this panel does not cache any items in advance which makes it faster on startup.
    /// Note that this panel does not maintain the order of the items due to optizing issues.
    /// </summary>
    public class VirtualizingItemsPanel : VirtualizingPanel, IOrderedChildren
    {
        private bool isLoaded = true;
        private List<UIElement> childrenIndex = new List<UIElement>();
        private double verticalOffset;
        private ScrollViewer scrollOwner;
        private int startIndex, lastIndex;
        private ItemsPresenter presenter;
        private int focusedIndex = -1;
        private DispatcherTimer timer;
        private int visibleItemsCount;


        public VirtualizingItemsPanel()
            : base()
        {
            this.Unloaded += new RoutedEventHandler(OnUnloaded);
            this.Loaded += new RoutedEventHandler(OnLoaded);
        }

        protected double VerticalOffsetInternal
        {
            get { return verticalOffset; }
        }

        /// <summary>
        /// Gets the ItemsPresenter which contains this panel.
        /// </summary>
        private ItemsPresenter Presenter
        {
            get { return VisualTreeHelper.GetParent(this) as ItemsPresenter; }
        }




        public bool IsIntervalEnabled
        {
            get { return (bool)GetValue(IsIntervalEnabledProperty); }
            set { SetValue(IsIntervalEnabledProperty, value); }
        }

        public static readonly DependencyProperty IsIntervalEnabledProperty =
            DependencyProperty.Register("IsIntervalEnabled", typeof(bool), typeof(VirtualizingItemsPanel), new PropertyMetadata(false));



        public TimeSpan Interval
        {
            get { return (TimeSpan)GetValue(IntervalProperty); }
            set { SetValue(IntervalProperty, value); }
        }

        public static readonly DependencyProperty IntervalProperty =
            DependencyProperty.Register("Interval", typeof(TimeSpan), typeof(VirtualizingItemsPanel), new PropertyMetadata(TimeSpan.FromMilliseconds(1)));



        /// <summary>
        /// Gets or sets the minimum items to load in advance in case of the height of the panel is not yet determined.
        /// </summary>
        public int MinimumItems
        {
            get { return (int)GetValue(MinimumItemsProperty); }
            set { SetValue(MinimumItemsProperty, value); }
        }

        public static readonly DependencyProperty MinimumItemsProperty =
            DependencyProperty.Register("MinimumItems", typeof(int), typeof(VirtualizingItemsPanel), new PropertyMetadata(0));


        /// <summary>
        /// Gets or sets the height for each items. The default value is 80.
        /// </summary>
        public double ItemHeight
        {
            get { return (double)GetValue(ItemHeightProperty); }
            set { SetValue(ItemHeightProperty, value); }
        }

        public static readonly DependencyProperty ItemHeightProperty =
            DependencyProperty.Register("ItemHeight", typeof(double), typeof(VirtualizingItemsPanel), new PropertyMetadata(80d));


        protected void StartTimer(bool restart = false)
        {
            if (!restart && (timer != null && timer.IsEnabled)) return;
            if (restart)
            {
                StopTimer();
                visibleItemsCount = 0;
            }
            timer = new DispatcherTimer
            {
                Interval = this.Interval,
            };
            timer.Tick += new EventHandler(OnTimerTick);
            timer.Start();
        }

        protected void StopTimer()
        {
            if (timer != null && timer.IsEnabled)
            {
                timer.Stop();
            }
        }

        private void OnTimerTick(object sender, EventArgs e)
        {
            visibleItemsCount++;
            InvalidateMeasure();
        }



        void OnLoaded(object sender, RoutedEventArgs e)
        {
            isLoaded = true;
            scrollOwner = this.GetVisualAncestors().OfType<ScrollViewer>().FirstOrDefault();
            if (scrollOwner != null)
            {
                presenter = scrollOwner.GetVisualDescendants().OfType<ItemsPresenter>().FirstOrDefault();

                Selector selector = ItemsControl.GetItemsOwner(this) as Selector;
                if (selector != null)
                {
                    selector.SelectionChanged += new SelectionChangedEventHandler(OnSelectionChanged);
                }
                //   scrollOwner.LayoutUpdated += new EventHandler(OnRender);
                CompositionTarget.Rendering += new EventHandler(OnRender);
            }
        }

        void OnUnloaded(object sender, RoutedEventArgs e)
        {
            isLoaded = false;
            StopTimer();
            Selector selector = ItemsControl.GetItemsOwner(this) as Selector;
            if (selector != null)
            {
                selector.SelectionChanged -= OnSelectionChanged;
            }
            //if (scrollOwner != null) scrollOwner.LayoutUpdated -= OnRender;
            CompositionTarget.Rendering -= OnRender;
        }

        /// <summary>
        /// It's important not to release the item which is currently selected. Otherwise
        /// this would cause to scroll to a position after that item is realized again because
        /// the Selector class uses a private _focusedIndex variable which tracks the selected item index
        /// even if the SelectedItem is set to null or SelectedIndex=-1.
        /// </summary>
        void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            Selector selector = sender as Selector;
            int focusedIndex = selector.SelectedIndex;
            if (focusedIndex >= 0) this.focusedIndex = focusedIndex;
        }


        void OnRender(object sender, EventArgs e)
        {
            double offset = scrollOwner.VerticalOffset;
            if (offset != verticalOffset || lastIndex < 0)
            {
                int firstVisibleItemIndex;
                int lastVisibleItemIndex;
                var itemsControl = ItemsControl;
                double height = itemsControl.ActualHeight;
                GetFirstAndLastIndex(height, out firstVisibleItemIndex, out lastVisibleItemIndex);

#if DEBUG
                bool mustInvalidate = firstVisibleItemIndex != startIndex || lastVisibleItemIndex != lastIndex;
#else
                bool mustInvalidate = firstVisibleItemIndex != startIndex || lastVisibleItemIndex != lastIndex;
                //bool mustInvalidate = firstVisibleItemIndex != startIndex || (lastIndex < 0 && lastVisibleItemIndex >= 0);
#endif
                verticalOffset = offset;
                if (mustInvalidate)
                {
                    InvalidateMeasure();
                }
            }
        }


        //TODO: optimize this:
        protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args)
        {
            base.OnItemsChanged(sender, args);
            switch (args.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    if (args.ItemUICount > 0)
                    {
                        RemoveAllItems();
                    }
                    break;
                case NotifyCollectionChangedAction.Remove:
                case NotifyCollectionChangedAction.Reset:
                    RemoveAllItems();
                    break;
            }
        }

        private void RemoveAllItems()
        {
            int count = Children.Count;
            if (count > 0)
            {
                IItemContainerGenerator generator = this.ItemContainerGenerator;
                generator.RemoveAll();
                RemoveInternalChildRange(0, count);
            }
            this.childrenIndex.Clear();
            startIndex = 0;
            lastIndex = -1;
        }

        protected override Size MeasureOverride(Size availableSize)
        {
            double availableHeight = availableSize.Height;
            double availableWidth = availableSize.Width;
            double itemHeight = ItemHeight;
            ItemsControl itemsControl = ItemsControl;
            int itemCount = itemsControl.Items != null ? itemsControl.Items.Count : 0;

            if (isLoaded)
            {
                FrameworkElement parent = VisualTreeHelper.GetParent(itemsControl) as FrameworkElement;

                if (double.IsInfinity(availableHeight)) availableHeight = parent.ActualHeight; // itemCount* itemHeight;
                if (double.IsInfinity(availableWidth)) availableWidth = 480.0;
                if (availableHeight == 0d && isLoaded)
                {
                    availableHeight = itemHeight * MinimumItems;
                }

                if (availableHeight > 0d)
                {
                    int firstVisibleItemIndex, lastVisibleItemIndex;
                    GetFirstAndLastIndex(availableHeight, out firstVisibleItemIndex, out lastVisibleItemIndex);

                    double itemWidth = availableWidth;

                    IItemContainerGenerator generator = this.ItemContainerGenerator;

                    CleanUpItems(firstVisibleItemIndex, lastVisibleItemIndex);
                    GeneratorPosition startPos = generator.GeneratorPositionFromIndex(firstVisibleItemIndex);
                    int childIndex = startPos.Offset == 0 ? startPos.Index : startPos.Index + 1;

                    childrenIndex.Clear();
                    Size size = new Size(itemWidth, itemHeight);
                    //this.RemoveInternalChildRange(0, Children.Count);

                    if (IsIntervalEnabled)
                    {
                        int delta = lastVisibleItemIndex - firstVisibleItemIndex;
                        if (delta > visibleItemsCount)
                        {
                            lastVisibleItemIndex = firstVisibleItemIndex + visibleItemsCount;
                            StartTimer(restart: false);
                        }
                        else
                        {
                            StopTimer();
                        }
                    }

                    using (generator.StartAt(startPos, GeneratorDirection.Forward, true))
                    {
                        for (int itemIndex = firstVisibleItemIndex; itemIndex <= lastVisibleItemIndex; ++itemIndex, ++childIndex)
                        {
                            bool newlyRealized;
                            UIElement child = generator.GenerateNext(out newlyRealized) as UIElement;
                            if (child != null)
                            {
                                childrenIndex.Add(child);
                                bool isAttached = Children.Contains(child);
                                if (!isAttached)
                                {
                                    if (childIndex >= Children.Count)
                                    {
                                        base.AddInternalChild(child);
                                    }
                                    else
                                    {
                                        base.InsertInternalChild(childIndex, child);
                                    }
                                    child.Measure(size);
                                }
                                generator.PrepareItemContainer(child);
                            }

                        }
                    }

                    CleanupUnusedItems();


                    double extentHeight = itemCount * itemHeight;
                    if (extentHeight < 0) throw new ArgumentOutOfRangeException();
                }
                else
                {
                    if (isLoaded) lastIndex = -1;
                }
            }
            return new Size(availableWidth, itemHeight * itemCount);
        }

        private void CleanupUnusedItems()
        {
            if (childrenIndex.Count < Children.Count)
            {
                for (int i = Children.Count - 1; i >= 0; i--)
                {
                    var c = Children[i];
                    if (!childrenIndex.Contains(c))
                    {
                        RemoveInternalChildRange(i, 1);
                    }
                }
            }
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            double itemHeight = ItemHeight;
            double width = finalSize.Width;

            Rect rect = new Rect(0, 0, width, itemHeight);
            for (int i = 0; i < childrenIndex.Count; i++)
            {
                UIElement c = childrenIndex[i];

                int itemIndex = startIndex + i;
                rect.Y = itemIndex * itemHeight;
                c.Arrange(rect);
            }

            return finalSize;
        }

        private ItemsControl itemsControl;

        protected ItemsControl ItemsControl
        {
            get
            {
                if (itemsControl == null) itemsControl = ItemsControl.GetItemsOwner(this);
                return itemsControl;
            }
        }

        protected override void OnClearChildren()
        {
            childrenIndex.Clear();
            base.OnClearChildren();
        }

        private void FindFocusedChild(out int focusedChild, out int previousFocusable, out int nextFocusable)
        {
            int num2;
            nextFocusable = num2 = -1;
            focusedChild = previousFocusable = num2;
            for (int i = 0; i < base.Children.Count; i++)
            {
                FrameworkElement element = base.Children[i] as FrameworkElement;
                if ((element != null) && HasFocus(element))
                {
                    focusedChild = i;
                    previousFocusable = i - 1;
                    nextFocusable = i + 1;
                    return;
                }
            }
        }

        private static bool HasFocus(DependencyObject obj)
        {
            for (DependencyObject obj2 = FocusManager.GetFocusedElement() as DependencyObject; obj2 != null; obj2 = VisualTreeHelper.GetParent(obj2))
            {
                if (object.ReferenceEquals(obj2, obj))
                {
                    return true;
                }
            }
            return false;
        }

        private void CleanUpItems(int firstVisibleItemIndex, int lastVisibleItemIndex)
        {
            // this is important, otherwise the item would get the DataContext of this panel, when recycled:
            this.DataContext = null;

            UIElementCollection children = this.Children;
            IItemContainerGenerator generator = this.ItemContainerGenerator;

            IRecyclingItemContainerGenerator recycle = generator as IRecyclingItemContainerGenerator;

            for (int i = children.Count - 1; i >= 0; i--)
            {
                // Map a child index to an item index by going through a generator position
                GeneratorPosition childGeneratorPos = new GeneratorPosition(i, 0);
                int itemIndex = generator.IndexFromGeneratorPosition(childGeneratorPos);
                if (itemIndex == focusedIndex)
                {
                    continue;
                }

                if (itemIndex >= 0)
                {
                    if (itemIndex < firstVisibleItemIndex || itemIndex > lastVisibleItemIndex)
                    {
                        if (recycle != null)
                        {
                            recycle.Recycle(childGeneratorPos, 1);
                        }
                        else
                        {
                            RemoveInternalChildRange(i, 1);
                            generator.Remove(childGeneratorPos, 1);
                        }
                    }
                }
            }

            this.startIndex = Math.Max(0, firstVisibleItemIndex);
            this.lastIndex = lastVisibleItemIndex;
        }

        public ScrollViewer ScrollOwner
        {
            get { return scrollOwner; }
            set
            {
                scrollOwner = value;
            }
        }

        private void GetFirstAndLastIndex(double availableHeight, out int firstVisibleItemIndex, out int lastVisibleItemIndex)
        {
            double itemHeight = ItemHeight;
            ItemsControl itemsControl = ItemsControl;
            int itemCount = itemsControl.Items != null ? itemsControl.Items.Count : 0;
            FrameworkElement parent = VisualTreeHelper.GetParent(itemsControl) as FrameworkElement;


            Rect? rect = this.GetBoundsRelativeTo(parent);
            double top = rect != null ? -rect.Value.Top : 0d;

            firstVisibleItemIndex = Math.Max(0, (int)(top / itemHeight));
            lastVisibleItemIndex = Math.Min((int)Math.Ceiling((top + availableHeight) / itemHeight), itemCount - 1);

        }

        protected override void BringIndexIntoView(int index)
        {
            base.BringIndexIntoView(index);
        }


        /// <summary>
        /// Gets all children ordered from top to bottom.
        /// </summary>
        /// <returns>Ordered children.</returns>
        public IEnumerable<DependencyObject> GetOrderedChildren()
        {
            return childrenIndex.Cast<DependencyObject>();
        }
    }

}
